{
  "nbformat": 4,
  "nbformat_minor": 0,
  "metadata": {
    "colab": {
      "provenance": []
    },
    "kernelspec": {
      "name": "python3",
      "display_name": "Python 3"
    },
    "language_info": {
      "name": "python"
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "source": [
        "<font color=\"#a9a56c\" size=2> **@Author: Arif Kasim Rozani - (Team Operation Badar)** </font>\n"
      ],
      "metadata": {
        "id": "38_QpUXJOAII"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Class Variable vs Instance Variable**"
      ],
      "metadata": {
        "id": "rVFqpArG3hjW"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Key Differences**\n",
        "\n",
        "\n",
        "| Feature      | Class Variable                                            | Instance Variable                          |   |\n",
        "|--------------|-----------------------------------------------------------|--------------------------------------------|---|\n",
        "| Scope        | Shared by all instances of the class                      | Unique to each instance of the class       |   |\n",
        "| Creation     | Created when the class is defined                         | Created when an object is instantiated     |   |\n",
        "| Access       | Accessed using the class name or instance name            | Accessed through an instance of the class  |   |\n",
        "| Modification | Modified using the class name                             | Modified through a specific instance       |   |\n",
        "| Use Cases    | Maintain values common to all instances, track statistics | Store object-specific data                 "
      ],
      "metadata": {
        "id": "v6IKcUnqyKi3"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "Understanding the distinction between class and instance variables is crucial for effective object-oriented programming in Python. Class variables provide a way to share data and behavior across all instances of a class, while instance variables allow each object to have its own unique state."
      ],
      "metadata": {
        "id": "fVh8oNn3zeRO"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **Composition and Aggregation**\n",
        "\n",
        "Composition and Aggregation are not design patterns — they are object-oriented design principles — how objects are built or connected."
      ],
      "metadata": {
        "id": "6574SKpN2kBm"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "### ✅ Composition (Strong Relationship — \"part of\")\n",
        "\n",
        "-   Think of a **car and its engine**.\n",
        "    \n",
        "-   A car **has-an** engine.\n",
        "    \n",
        "-   If the **car is destroyed**, the **engine is gone too**.\n",
        "    \n",
        "-   The engine **can’t exist on its own** — it’s part of the car.\n",
        "    \n",
        "-   In Python, this means one class creates and **owns** objects of another class."
      ],
      "metadata": {
        "id": "hR5fMwnM4k9N"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class Engine:\n",
        "    def start(self):\n",
        "        print(\"Engine starts\")\n",
        "\n",
        "class Car:\n",
        "    def __init__(self):\n",
        "        self.engine = Engine()  # Car owns the Engine\n",
        "\n",
        "    def drive(self):\n",
        "        self.engine.start()\n",
        "\n",
        "my_car = Car()\n",
        "my_car.drive()\n"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "uR93HXMx4v7k",
        "outputId": "82d7d38c-d488-48e9-8a9b-12ecaa049059"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Engine starts\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "<font color=orange>If my_car is deleted, the engine is also gone — they are tightly connected.</font>"
      ],
      "metadata": {
        "id": "5Kvqurhi44mt"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "\n",
        "### ✅ Aggregation (Weak Relationship — \"connected to\")\n",
        "\n",
        "-   Think of a **school and its students**.\n",
        "    \n",
        "-   A school **has students**, but students can **exist without** the school.\n",
        "    \n",
        "-   If the school closes, the students still **exist elsewhere**.\n",
        "    \n",
        "-   In Python, this means one class is **linked** to another, but doesn’t own it."
      ],
      "metadata": {
        "id": "fNBFQaEj5Dv1"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class Student:\n",
        "    def __init__(self, name):\n",
        "        self.name = name\n",
        "\n",
        "class School:\n",
        "    def __init__(self, students):\n",
        "        self.students = students  # School does not own students\n",
        "\n",
        "student1 = Student(\"Ali\")\n",
        "student2 = Student(\"Sara\")\n",
        "my_school = School([student1, student2])\n",
        "\n",
        "print(\"School = \", my_school)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "r6xphLWI5LO2",
        "outputId": "5c1a5ccf-0a5b-4826-f487-bc923d206689"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "School =  <__main__.School object at 0x7c7b306b23d0>\n"
          ]
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "del my_school"
      ],
      "metadata": {
        "id": "ZKzpVrsA5nqt"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "source": [
        "print(\"Student1 = \", student1)\n",
        "print(\"Student2 = \", student2)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "IbTwNMmt5qCc",
        "outputId": "22813977-ae26-489f-b307-ea15471e21b4"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Student1 =  <__main__.Student object at 0x7c7b306b25d0>\n",
            "Student2 =  <__main__.Student object at 0x7c7b306b2510>\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "<font color=orange>Here, even if my_school is deleted, student1 and student2 still exist — they’re independent.</font>"
      ],
      "metadata": {
        "id": "jnMBO65j5WHE"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **Method Resolution Order (MRO) and Diamond Inheritance**\n",
        "\n",
        "**Method Resolution Order (MRO)** is the order in which Python searches for methods and attributes in a class hierarchy, especially in cases of multiple inheritance. It ensures that the correct method or attribute is found and called `when there are overlapping names in the inheritance tree`."
      ],
      "metadata": {
        "id": "408uEHx_cyAW"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Example: Understanding MRO in Python**\n",
        "\n",
        "Let’s create a class hierarchy with multiple inheritance to demonstrate how MRO works."
      ],
      "metadata": {
        "id": "tNRIV1zGdPT0"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "# Define the classes\n",
        "class A:\n",
        "    def greet(self):\n",
        "        return \"Hello from A\"\n",
        "\n",
        "class B(A):\n",
        "    def greet(self):\n",
        "        return \"Hello from B\"\n",
        "\n",
        "class C(A):\n",
        "    def greet(self):\n",
        "        return \"Hello from C\"\n",
        "\n",
        "class D(B, C): # Diamond Inheritance\n",
        "    pass\n",
        "\n",
        "# Create an instance of D\n",
        "d = D()\n",
        "\n",
        "# Check the MRO of class D\n",
        "print(D.mro())  # Output: [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]\n",
        "\n",
        "# Call the greet method\n",
        "print(d.greet())  # Output: Hello from B\n",
        "print(\"The MRO for class D can be visualized as a linear sequence: D → B → C → A → object\")"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "qJHLLxvldR-D",
        "outputId": "dc45a06b-40cf-4cef-b588-219df7158fa1"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]\n",
            "Hello from B\n",
            "The MRO for class D can be visualized as a linear sequence: D → B → C → A → object\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "\n",
        "### 🎯 KEY POINT:\n",
        "\n",
        "> **“When there are overlapping names in the inheritance tree”** means **more than one parent class** defines the **same method or attribute name**.  \n",
        "> Python then uses **MRO** to figure out **which one to call** — avoiding confusion and errors."
      ],
      "metadata": {
        "id": "B17yx1KD8y9z"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "[The Veiled Virgin](https://en.wikipedia.org/wiki/The_Veiled_Virgin)"
      ],
      "metadata": {
        "id": "dl4WGH_vb4mn"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "![Veiled_virgin.jpg]()"
      ],
      "metadata": {
        "id": "ubFBARXkbqv6"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "<font color=orange>The Veiled(vayld) Virgin looks like it’s wearing a soft veil, but it’s all carved from one solid rock—just like a decorator adds something extra to a function without changing it. The function stays the same underneath, but the decorator adds a special effect. It’s like dressing up your code without touching its heart.</font>\n",
        "\n",
        "\n",
        "\n",
        "\n",
        "\n",
        "\n"
      ],
      "metadata": {
        "id": "wHuPovIgc-RB"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **Decorators in Classes**\n",
        "\n",
        "Decorators in Python are a powerful feature that allows you to modify or extend the behavior of functions or methods. When applied to classes, decorators can enhance or alter the behavior of the class or its methods. Additionally, Python provides specific property decorators (@property, @setter, and @deleter) to manage attribute access in a controlled way."
      ],
      "metadata": {
        "id": "fR0DihIrfbpK"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Function Decorators**"
      ],
      "metadata": {
        "id": "STfVl90gC4s9"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "def star_decorator(func):   # say_hello() will passed as a parameter\n",
        "    def wrapper():\n",
        "        print(\"★\" * 5)\n",
        "        func()\n",
        "        print(\"★\" * 5)\n",
        "    return wrapper\n",
        "\n",
        "@star_decorator\n",
        "def say_hello():\n",
        "    print(\"Hello!\")\n",
        "\n",
        "say_hello()"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "A46Ye0uoDFat",
        "outputId": "e01a89be-914d-4ce1-963a-c69bc7762ce9"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "★★★★★\n",
            "Hello!\n",
            "★★★★★\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Class Decorators**"
      ],
      "metadata": {
        "id": "7-JfxqL1fnTz"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class ObjectCounter:\n",
        "    def __init__(self, cls):\n",
        "        print(\"Decorator Applied: \", cls)\n",
        "        self.cls = cls\n",
        "        self.count = 0\n",
        "\n",
        "    def __call__(self, *args, **kwargs):\n",
        "        self.count += 1\n",
        "        print(f\"{self.cls.__name__} object created {self.count} times\")\n",
        "        return self.cls(*args, **kwargs)"
      ],
      "metadata": {
        "id": "loxqDwY_Eev5"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "source": [
        "@ObjectCounter\n",
        "class Animal:\n",
        "  pass"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "ps4lR2SODZNc",
        "outputId": "0162aebd-78a3-44da-e0ec-cdcb991b7b7d"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Decorator Applied:  <class '__main__.Animal'>\n"
          ]
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "# Create instances\n",
        "a = Animal()\n",
        "b = Animal()\n",
        "c = Animal()"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "x8AIJ9emFiTT",
        "outputId": "0f917451-a629-4f77-ac17-429097238b1d"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Animal object created 1 times\n",
            "Animal object created 2 times\n",
            "Animal object created 3 times\n"
          ]
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "@ObjectCounter\n",
        "class Car:\n",
        "  pass"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "Ko8W6jj1EYl0",
        "outputId": "9eeabdf1-7b3d-42cc-c834-33f53beaa079"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Decorator Applied:  <class '__main__.Car'>\n"
          ]
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "car1 = Car()\n",
        "car2 = Car()"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "JaHXcRO_FnnK",
        "outputId": "7006b077-397c-4dde-906d-de09cded3a4d"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Car object created 1 times\n",
            "Car object created 2 times\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Property Decorators**\n",
        "\n",
        " <font color=orange>It allows you to access the attribute like a property rather than a method. You can also define setter and **deleter** methods using the @setter and @deleter decorators, respectively.\n",
        "\n",
        "\n",
        "property decorators like `@property`, `@setter`, and `@deleter` are **built-in** in Python.\n",
        "</font>"
      ],
      "metadata": {
        "id": "90yUMeHKfslX"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "### **1️⃣ Basic Getter (Read-Only Property)**"
      ],
      "metadata": {
        "id": "KABPdm3RvGnD"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class Person:\n",
        "    def __init__(self, name):\n",
        "        self._name = name  # Internal variable (convention: `_name`)\n",
        "\n",
        "    @property\n",
        "    def name(self):\n",
        "        \"\"\"Getter for name\"\"\"\n",
        "        return self._name\n",
        "\n",
        "# Usage\n",
        "person = Person(\"Alice\")\n",
        "print(\"person.name: \", person.name)  # Like an attribute (no parentheses!)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "HCdqNp6WvUpi",
        "outputId": "ffc1ca57-c52b-43da-82f8-0d5180436244"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "person.name:  Alice\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "### **2️⃣ Setter (Change a Value with Validation)**"
      ],
      "metadata": {
        "id": "3ba69XhmvbpJ"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class Person:\n",
        "    def __init__(self, name):\n",
        "        self._name = name\n",
        "\n",
        "    @property\n",
        "    def name(self):\n",
        "        return self._name\n",
        "\n",
        "    @name.setter\n",
        "    def name(self, new_name):\n",
        "        self._name = new_name\n",
        "\n",
        "# Usage\n",
        "person = Person(\"Bob\")\n",
        "print(\"Before - person.name: \",person.name)\n",
        "person.name = \"Charlie\"  # Works\n",
        "print(\"After  - person.name: \",person.name)  # Output: Charlie"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "lnDqiXf1vdpS",
        "outputId": "35068b30-2dbf-4f4b-f9b1-4aabbf142631"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Before - person.name:  Bob\n",
            "After  - person.name:  Charlie\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "### **3️⃣ Deleter (Remove an Attribute)**"
      ],
      "metadata": {
        "id": "OLiIPoQmvwfr"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class Person:\n",
        "    def __init__(self, name):\n",
        "        self._name = name\n",
        "\n",
        "    @property\n",
        "    def name(self):\n",
        "        return self._name\n",
        "\n",
        "    @name.deleter\n",
        "    def name(self):\n",
        "        print(\"Deleting name!\")\n",
        "        del self._name\n",
        "\n",
        "# Usage\n",
        "person = Person(\"Dave\")\n",
        "print(person.name)  # Output: Dave\n",
        "\n",
        "del person.name  # Runs deleter"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "YXXtT1j4vzh5",
        "outputId": "55b00835-9256-42a2-ac46-21b77c35cde3"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Dave\n",
            "Deleting name!\n"
          ]
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "#print(person.name)  # ❌ Error! (AttributeError: 'Person' has no attribute '_name') # uncomment to see error"
      ],
      "metadata": {
        "id": "u9u1W_FVH7c6"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "\n",
        "### **4️⃣ Computed Property (Dynamic Value)**"
      ],
      "metadata": {
        "id": "C1Jtm2ZTwAj4"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class Person:\n",
        "    def __init__(self, weight_kg, height_m):\n",
        "        self.weight = weight_kg\n",
        "        self.height = height_m\n",
        "\n",
        "    @property\n",
        "    def bmi(self):\n",
        "        \"\"\"Body Mass Index (weight / height²)\"\"\"\n",
        "        return self.weight / (self.height ** 2)\n",
        "\n",
        "# Usage\n",
        "p = Person(70, 1.75)  # 70kg, 1.75m\n",
        "print(p.bmi)  # Output: 22.857..."
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "-1K3qzk7wCtx",
        "outputId": "702522c8-8c21-42db-9abc-331e1539bf70"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "22.857142857142858\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **What is Callable**\n",
        "\n",
        "In Python, a `callable` is an object that can be called like a function. In other words, it's an object that can be invoked with parentheses `()` to execute some code.\n",
        "\n",
        "\n",
        "**What makes an object callable?**\n",
        "\n",
        "An object is callable if it has a `__call__` method. This method is a special method that's invoked when you call the object like a function.\n",
        "\n",
        "**Example:**"
      ],
      "metadata": {
        "id": "n2oz2QRJ2xEB"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class MyClass:\n",
        "    def __call__(self):\n",
        "        print(\"I'm callable!\")\n",
        "\n",
        "obj = MyClass()\n",
        "obj()  # Output: I'm callable!"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "FU_PLF3B3H8Y",
        "outputId": "a96e28c1-11eb-4530-9b18-57c0ca04f1b7"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "I'm callable!\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "\n",
        "\n",
        "**Checking if an object is callable**\n",
        "\n",
        "You can use the `callable()` function to check if an object is callable:"
      ],
      "metadata": {
        "id": "ZMAMna0d3Tpg"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "def my_function():\n",
        "    pass\n",
        "\n",
        "print(callable(my_function))  # Output: True\n",
        "\n",
        "class MyClass:\n",
        "    def __call__(self):\n",
        "        pass\n",
        "\n",
        "obj = MyClass()\n",
        "print(callable(obj))  # Output: True\n",
        "\n",
        "print(callable(\"hello\"))  # Output: False"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "_JSWJXFn3ZMw",
        "outputId": "14eebfd3-05c7-494c-fcf2-68236bd70098"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "True\n",
            "True\n",
            "False\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **Working with Modules and Packages in OOP**\n",
        "\n",
        "Organizing your code into **modules** and **packages** is essential for maintaining clean, scalable, and maintainable code, especially in large projects. In Python, a **module** is a single file containing Python code (e.g., classes, functions, variables), and a **package** is a directory containing multiple modules and an \\_\\_init__.py file."
      ],
      "metadata": {
        "id": "JCrYdQ4Ak2aN"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "\n",
        "### 📦 What is a Package?\n",
        "\n",
        "-   A **package** is just a **folder** that contains **Python modules** (other `.py` files).\n",
        "    \n",
        "-   To be recognized as a **package**, that folder **must have** a special file: `__init__.py`."
      ],
      "metadata": {
        "id": "DOKpNvTfOX3r"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "\n",
        "### 🧾 So, what does `__init__.py` do?\n",
        "\n",
        "1.  ✅ **Marks the folder as a package**  \n",
        "    Without `__init__.py`, Python won’t treat the folder as a proper package you can import from.\n",
        "    \n",
        "2.  📦 **Runs initialization code**  \n",
        "    If you want some code to run **when the package is imported**, you can put it inside `__init__.py`.\n",
        "    \n",
        "3.  🪄 **Control what gets exposed**  \n",
        "    You can use it to control what parts of the package are visible when someone does:"
      ],
      "metadata": {
        "id": "oonB8a3nOjMz"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "```\n",
        "mypackage/\n",
        "├── __init__.py\n",
        "├── math_utils.py\n",
        "├── string_utils.py\n",
        "```\n"
      ],
      "metadata": {
        "id": "SOIW3LFoOsfL"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "```python\n",
        "from mypackage import *\n",
        "```\n"
      ],
      "metadata": {
        "id": "xGIc3ABjO4ay"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **Advanced OOP Concepts**\n",
        "\n",
        "In this section, we’ll explore some advanced Object-Oriented Programming (OOP) concepts in Python, including **metaclasses**, **design patterns** (Singleton and Factory), and how to implement them."
      ],
      "metadata": {
        "id": "OuJIeervo1IU"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **1. Metaclasses**\n",
        "\n",
        "A metaclass is the class of a class. It defines how a class behaves. In Python, the default metaclass is type. You can create custom metaclasses to control class creation and behavior."
      ],
      "metadata": {
        "id": "hO7GpHTYpAFS"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **2. Singleton Design Pattern**\n",
        "\n",
        "The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This is useful when you need to manage shared resources, such as a database connection or configuration settings."
      ],
      "metadata": {
        "id": "GynKBXLZpH2q"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **3. Factory Design Pattern**\n",
        "\n",
        "The Factory pattern is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. It promotes loose coupling and flexibility."
      ],
      "metadata": {
        "id": "_H8YaIw4pLjK"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **To know more about Design Pattern please visit:**\n",
        "\n",
        "[The Catalog of Python Design Pattern Examples](https://refactoring.guru/design-patterns/python)\n",
        "\n",
        "[Gang of Four design patterns in Python](https://github.com/tuvo1106/python_design_patterns)\n",
        "\n",
        "[Gang of Four (GOF) Design Patterns](https://www.geeksforgeeks.org/gang-of-four-gof-design-patterns/)"
      ],
      "metadata": {
        "id": "onqIIiRdpoS5"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Example: Implementing Advanced OOP Concepts in Python**\n",
        "\n",
        "Let’s create examples to demonstrate metaclasses, the Singleton pattern, and the Factory pattern."
      ],
      "metadata": {
        "id": "fWxlFf7wrwyY"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Metaclass Example**"
      ],
      "metadata": {
        "id": "Rh8g5lujr0if"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "# Custom metaclass\n",
        "class Meta(type):\n",
        "    def __new__(cls, name, bases, dct):\n",
        "        print(f\"Creating class: {name}\")\n",
        "        return super().__new__(cls, name, bases, dct)\n",
        "\n",
        "# Class using the custom metaclass\n",
        "class MyClass(metaclass=Meta):\n",
        "    pass\n",
        "\n",
        "# Output: Creating class: MyClass"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "69HDFssYr254",
        "outputId": "dc256267-34b0-4ed2-b78d-f515301857ec"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Creating class: MyClass\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Singleton Design Pattern Example**"
      ],
      "metadata": {
        "id": "gL9Yppo4sADI"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class Singleton:\n",
        "    _instance = None\n",
        "\n",
        "    def __new__(cls, *args, **kwargs):\n",
        "        if cls._instance is None:\n",
        "            cls._instance = super().__new__(cls)\n",
        "        return cls._instance\n",
        "\n",
        "# Create instances of the Singleton class\n",
        "singleton1 = Singleton()\n",
        "singleton2 = Singleton()\n",
        "\n",
        "# Check if both instances are the same\n",
        "print(singleton1 is singleton2)  # Output: True\n",
        "print(id(singleton1) == id(singleton2))  # Output: True"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "UrOglgljsBeP",
        "outputId": "59b32d07-fcdd-4730-e1ab-eafdff67ab69"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "True\n",
            "True\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Factory Design Pattern Example**"
      ],
      "metadata": {
        "id": "YIOP3DTNsP6n"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "# Product interface\n",
        "class Animal:\n",
        "    def speak(self):\n",
        "        pass\n",
        "\n",
        "# Concrete products\n",
        "class Dog(Animal):\n",
        "    def speak(self):\n",
        "        return \"Woof!\"\n",
        "\n",
        "class Cat(Animal):\n",
        "    def speak(self):\n",
        "        return \"Meow!\"\n",
        "\n",
        "# Factory class\n",
        "class AnimalFactory:\n",
        "    @staticmethod\n",
        "    def create_animal(animal_type):\n",
        "        if animal_type == \"dog\":\n",
        "            return Dog()\n",
        "        elif animal_type == \"cat\":\n",
        "            return Cat()\n",
        "        else:\n",
        "            raise ValueError(\"Invalid animal type\")\n",
        "\n",
        "# Use the factory to create animals\n",
        "dog = AnimalFactory.create_animal(\"dog\")\n",
        "cat = AnimalFactory.create_animal(\"cat\")\n",
        "\n",
        "# Call the speak method\n",
        "print(dog.speak())  # Output: Woof!\n",
        "print(cat.speak())  # Output: Meow!"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "G6WObb5ssR23",
        "outputId": "dd463319-45da-4ed6-f486-67641a807b13"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Woof!\n",
            "Meow!\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **Error Handling in OOP - Creating Custom Exception**\n",
        "\n",
        "Error handling is a critical aspect of writing robust and reliable code. In Object-Oriented Programming (OOP), you can handle errors by raising exceptions in methods and creating custom exceptions to represent specific error conditions in your application."
      ],
      "metadata": {
        "id": "mB000GdktcIm"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Code Example**"
      ],
      "metadata": {
        "id": "bi9pZk8IwN01"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "# Custom exception for insufficient funds\n",
        "class InsufficientFundsError(Exception):\n",
        "    def __init__(self, balance, amount):\n",
        "        super().__init__(f\"Insufficient funds: balance is {balance}, but {amount} was requested.\")\n",
        "        self.balance = balance\n",
        "        self.amount = amount"
      ],
      "metadata": {
        "id": "pocrplPDwRa9"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "source": [
        "car_price = 10000\n",
        "balance = 50000 # Replace 50000 with 50\n",
        "\n",
        "if balance < car_price:\n",
        "    raise InsufficientFundsError(balance, car_price)\n",
        "\n",
        "print(\"Yes! I can buy a car.\")"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "1yMP36cLULyw",
        "outputId": "94c3fa28-28b1-4327-bacb-84e404bdea7e"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Yes! I can buy a car.\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **Testing OOP Code**\n",
        "\n",
        "Testing is a critical part of software development, especially when working with Object-Oriented Programming (OOP). It ensures that your classes and methods behave as expected. Python provides libraries like unittest for writing and running tests."
      ],
      "metadata": {
        "id": "OIwbXoSbx-Wj"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Unit Testing Classes and Methods**\n",
        "\n",
        "**Unit testing** involves testing individual components (e.g., classes and methods) in isolation to ensure they work correctly."
      ],
      "metadata": {
        "id": "5tvqB7NdyE2q"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Calculator Class**"
      ],
      "metadata": {
        "id": "SlZnWo79yfxR"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class Calculator:\n",
        "    def add(self, a, b):\n",
        "        return a + b\n",
        "\n",
        "    def subtract(self, a, b):\n",
        "        return a - b\n",
        "\n",
        "    def multiply(self, a, b):\n",
        "        return a * b\n",
        "\n",
        "    def divide(self, a, b):\n",
        "        if b == 0:\n",
        "            raise ValueError(\"Cannot divide by zero.\")\n",
        "        return a / b"
      ],
      "metadata": {
        "id": "wcNj0JCDyhyU"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Testing with unittest**"
      ],
      "metadata": {
        "id": "xf5HpZlayl6C"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "import unittest\n",
        "#from calculator import Calculator\n",
        "\n",
        "class TestCalculator(unittest.TestCase):\n",
        "    def setUp(self):\n",
        "        self.calc = Calculator()\n",
        "\n",
        "    def test_add(self):\n",
        "        self.assertEqual(self.calc.add(2, 3), 5)\n",
        "\n",
        "    def test_subtract(self):\n",
        "        self.assertEqual(self.calc.subtract(5, 3), 2)\n",
        "\n",
        "    def test_multiply(self):\n",
        "        self.assertEqual(self.calc.multiply(2, 3), 6)\n",
        "\n",
        "    def test_divide(self):\n",
        "        self.assertEqual(self.calc.divide(6, 3), 2)\n",
        "\n",
        "    def test_divide_by_zero(self):\n",
        "        with self.assertRaises(ValueError):\n",
        "            self.calc.divide(6, 0)\n",
        "\n",
        "# Replace unittest.main() with the following to run tests in IPython:\n",
        "# if __name__ == \"__main__\":\n",
        "#     unittest.main()\n",
        "# Instead, use:\n",
        "if __name__ == '__main__':\n",
        "    unittest.main(argv=['first-arg-is-ignored'], exit=False)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "RtJOAJEtynQB",
        "outputId": "bc4cc0bd-8d51-48cd-a431-de21692dea80"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stderr",
          "text": [
            ".....\n",
            "----------------------------------------------------------------------\n",
            "Ran 5 tests in 0.003s\n",
            "\n",
            "OK\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **Iterable**"
      ],
      "metadata": {
        "id": "AJrmBy8pgzSO"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "In Python, Iterable is not a parent class but rather an abstract base class (ABC) defined in the collections.abc module. It serves as a protocol or interface that other classes can implement to indicate that they are iterable (i.e., they can be looped over using a for loop or other iteration constructs).\n",
        "\n",
        "## **What is an Iterable?**\n",
        "\n",
        "An iterable is any object that can return an iterator when the iter() function is called on it. The iterator is used to traverse through the elements of the iterable.\n",
        "\n",
        "## **Parent Class Relationship**\n",
        "\n",
        "The Iterable abstract base class is not a parent class in the traditional sense (like inheritance in object-oriented programming). Instead, it is used to define a protocol that other classes can adhere to by implementing the __iter__() method.\n"
      ],
      "metadata": {
        "id": "kFvkPBs1g9aB"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Example of Iterable Classes**\n",
        "\n",
        "Many built-in Python classes are iterable because they implement the __iter__() method. These include:\n",
        "\n",
        "1.  **Lists**: list\n",
        "2.  **Tuples**: tuple\n",
        "3.  **Strings**: str\n",
        "4.  **Dictionaries**: dict\n",
        "5.  **Sets**: set\n",
        "6.  **Ranges**: range\n",
        "7.  **Generators**: generator\n",
        "\n",
        "<br>\n",
        "\n",
        "## **How to Check if a Class is Iterable**\n",
        "\n",
        "You can use the isinstance() function with collections.abc.Iterable to check if an object is iterable:"
      ],
      "metadata": {
        "id": "h-GrGfjYhI3w"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from collections.abc import Iterable\n",
        "\n",
        "# Check if built-in types are iterable\n",
        "print(\"isinstance([1, 2, 3], Iterable) = \",isinstance([1, 2, 3], Iterable))  # True (list is iterable)\n",
        "print('isinstance(\"hello\", Iterable)   = ', isinstance(\"hello\", Iterable))    # True (string is iterable)\n",
        "print(\"isinstance(123, Iterable)       = \", isinstance(123, Iterable))        # False (integer is not iterable)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "9ybe-M8shdPw",
        "outputId": "b450b6b8-4f5e-4545-db1b-e9cccb0cdc28"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "isinstance([1, 2, 3], Iterable) =  True\n",
            "isinstance(\"hello\", Iterable)   =  True\n",
            "isinstance(123, Iterable)       =  False\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **How to Make a Custom Class Iterable**\n",
        "\n",
        "To make a custom class iterable, you need to implement the __iter__() method, which should return an iterator object. The iterator object must implement the __next__() method.\n",
        "\n",
        "\n",
        "### 🔄 What is Iteration in Python?\n",
        "\n",
        "-   Iteration is just a **fancy word for looping** — like when you use a `for` loop.\n",
        "    \n",
        "-   Behind the scenes, Python uses something called **iterators** to do this looping.\n",
        "\n",
        "## **Example:**"
      ],
      "metadata": {
        "id": "KZxNI4OQhh5w"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from collections.abc import Iterable, Iterator\n",
        "\n",
        "class MyCollection(Iterable):\n",
        "    def __init__(self, collection_data):\n",
        "        self.collection_data = collection_data\n",
        "\n",
        "    def __iter__(self):\n",
        "        return MyIterator(self.collection_data)\n",
        "\n",
        "class MyIterator(Iterator):\n",
        "    def __init__(self, collection_data):\n",
        "        self.collection_data = collection_data\n",
        "        self.index = 0\n",
        "\n",
        "    def __next__(self):\n",
        "        if self.index >= len(self.collection_data):\n",
        "            raise StopIteration # Builtin Exception in Python: https://docs.python.org/3/library/exceptions.html\n",
        "        value = self.collection_data[self.index]\n",
        "        self.index += 1\n",
        "        print(\"MyIterator.__next__ Called\")\n",
        "        return value\n",
        "\n",
        "# Usage\n",
        "my_iterable = MyCollection([1, 2, 3])\n",
        "for item in my_iterable:\n",
        "    print(\"item : \",item)  # Output: 1, 2, 3"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "Him3BqMrh7CA",
        "outputId": "cf47bdbd-d7d6-4353-c1ed-bf5745c2d4f8"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "MyIterator.__next__ Called\n",
            "item :  1\n",
            "MyIterator.__next__ Called\n",
            "item :  2\n",
            "MyIterator.__next__ Called\n",
            "item :  3\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "\n",
        "### 🚫 What is `raise StopIteration`?\n",
        "\n",
        "-   `raise StopIteration` is used to **tell Python to stop looping**.\n",
        "    \n",
        "-   It’s like saying: **“I’m done giving you more items!”**\n",
        "    \n",
        "\n",
        "This is used **inside custom iterators** — you’ll rarely write it unless you’re making your own."
      ],
      "metadata": {
        "id": "vYd7pb73Zyui"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Key Points**\n",
        "\n",
        "* Iterable is an abstract base class (ABC) from the collections.abc module.\n",
        "\n",
        "* It defines a protocol for iterable objects by requiring the implementation of the __iter__() method.\n",
        "* Many built-in Python classes (e.g., list, tuple, str, dict) are iterable because they implement this protocol.\n",
        "* Custom classes can be made iterable by implementing the __iter__() method."
      ],
      "metadata": {
        "id": "O0ZJLQHdixKx"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **Object-Based Language vs. Object-Oriented Language**"
      ],
      "metadata": {
        "id": "dQGGiPmudbQN"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Object-Based Language**\n",
        "\n",
        "  * **Definition**: A language that supports objects (data structures with attributes and methods) and encapsulation (data hiding), but lacks key OOP features like inheritance and polymorphism.\n",
        "  * **Features**:\n",
        "    * Objects as instances with properties and methods.\n",
        "    * Encapsulation (e.g., public/private access modifiers).\n",
        "    * May include basic polymorphism (e.g., operator overloading).\n",
        "  * Examples: JavaScript (prototype-based, but lacks classical inheritance), classic Visual Basic, Ada."
      ],
      "metadata": {
        "id": "M5-z-Jz9dgB2"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Object-Oriented Language**\n",
        "\n",
        "  * **Definition**: A language that implements the four pillars of OOP:\n",
        "    1.  **Encapsulation**: Hiding internal state and requiring interaction via methods.\n",
        "    2.  **Inheritance**: Creating hierarchical relationships between classes (e.g., subclasses reusing parent class code).\n",
        "    3.  **Polymorphism**: Allowing objects of different classes to respond to the same method (via inheritance or interfaces).\n",
        "    4.  **Abstraction**: Simplifying complexity through abstract classes/interfaces.\n",
        "\n",
        "  * Examples: Python, Java, C++, C#."
      ],
      "metadata": {
        "id": "RUDyR5qldyg-"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **The Python's Object-Centric Nature**"
      ],
      "metadata": {
        "id": "DRDYhz5VeLok"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **Is Everything in Python an Object? YES!**\n",
        "\n",
        "**Yes, in Python, absolutely everything is an object. This is a fundamental characteristic of the language and a core design principle.**\n",
        "\n",
        "  * **Numbers**: Integers, floats, complex numbers are objects.\n",
        "  * **Strings**: Textual data is represented as string objects.\n",
        "  * **Lists**, Tuples, Dictionaries, Sets: These are built-in container types and are all objects.\n",
        "  * **Functions**: Functions are first-class objects in Python. You can assign them to variables, pass them as arguments to other functions, and even return them from functions.\n",
        "  * **Classes and Modules**: Classes themselves are objects (instances of metaclasses), and modules are also objects.\n",
        "  * **Even None**: None, which represents the absence of a value, is an object of the NoneType class.\n",
        "  * **Types/Classes**: In Python, types (like int, str, list) are also objects (they are instances of the metaclass type).\n",
        "\n",
        "\n",
        "## **How to Verify:**\n",
        "\n",
        "You can use the type() function in Python to check the type of any entity. It will always return a class (which is itself an object)."
      ],
      "metadata": {
        "id": "yXVSTiwqeWay"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "``` python\n",
        ">>> type(5)\n",
        "<class 'int'>\n",
        ">>> type(\"hello\")\n",
        "<class 'str'>\n",
        ">>> type([1, 2, 3])\n",
        "<class 'list'>\n",
        ">>> def my_function():\n",
        "...     pass\n",
        ">>> type(my_function)\n",
        "<class 'function'>\n",
        ">>> class MyClass:\n",
        "...     pass\n",
        ">>> obj = MyClass()\n",
        ">>> type(MyClass)\n",
        "<class 'type'>  # Classes are instances of 'type' (metaclass)\n",
        ">>> type(obj)\n",
        "<class '__main__.MyClass'>\n",
        "\n",
        "```"
      ],
      "metadata": {
        "id": "Kafip9bifCR2"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Why is this important in Python?**\n",
        "\n",
        "  * **Consistency**: It creates a consistent and unified way to work with data and code. Everything behaves like an object, leading to a more predictable programming model.\n",
        "  \n",
        "  * **Flexibility**: Because functions and classes are objects, Python is highly dynamic and allows for powerful meta-programming techniques. You can inspect, modify, and create objects dynamically at runtime.\n",
        "  * **Object-Oriented Programming**: This \"everything is an object\" nature is foundational to Python's object-oriented features. It makes it natural to work with classes, inheritance, and polymorphism, as all entities are treated as objects."
      ],
      "metadata": {
        "id": "7L8AdoKRfQCT"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **In summary:**\n",
        "\n",
        "  * Object-based languages provide objects and some basic object-related features but lack the full suite of OOP principles.\n",
        "\n",
        "  * Object-oriented languages fully embrace OOP by incorporating classes, encapsulation, abstraction, inheritance, and polymorphism, leading to better software design and organization.\n",
        "  * **Python is a fully object-oriented language where everything is an object**, contributing to its flexibility, consistency, and power."
      ],
      "metadata": {
        "id": "M-uvnOVQfgJW"
      }
    }
  ]
}